package com.googlecode.horncomb.orm.tool.codegen.ant;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.CallTarget;
import org.apache.tools.ant.taskdefs.Property;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.googlecode.horncomb.orm.id.EntityIdUtil;

public class JpaEntityToXmlTask extends Task {
	static final String NODE_ROOT = "entity-mappings";
	static final String NODE_PACKAGE = "package";

	static final String NODE_ENTITY = "entity";
	static final String ENTITY_ATTR_CLASS = "class";
	static final String ENTITY_ATTR_ACCESS = "access";
	static final String ENUM_ACCESS_PROPERTY = "PROPERTY";
	static final String ENUM_ACCESS_FIELD = "FIELD";

	static final String NODE_TABLE = "table";
	static final String TABLE_NAME = "name";

	static final String NODE_ATTRIBUTES = "attributes";

	static final String NODE_ID = "id";
	static final String ID_NAME = "name";

	static final String NODE_BASIC = "basic";
	static final String BASIC_NAME = "name";

	static final String NODE_COLUMN = "column";
	static final String COLUMN_NAME = "name";

	private String includesClass = null;
	private String classBasedir = null;
	private String encoding = "UTF-8";
	private String outputBasedir = null;
	private String extension = ".xml";

	private String outputDirId;
	private String outputFileId;

	private String target;

	private boolean overwrite = false;
	private String defaultAccessType = ENUM_ACCESS_PROPERTY;

	private Map<String, Element> attrCache = new HashMap<String, Element>();
	private Map<String, Element> colCache = new HashMap<String, Element>();

	@Override
	public void execute() throws BuildException {
		this.log("Begin Run " + this.getClass().getSimpleName(),
				Project.MSG_INFO);
		Assert.hasText(classBasedir, "The attribute[classBasdir] is needed");
		DirectoryScanner scanner = new DirectoryScanner();
		scanner.setBasedir(new File(classBasedir));
		scanner.scan();
		String[] filenames = scanner.getIncludedFiles();
		for (String filename : filenames) {
			String classFilename = filename.replaceAll("\\\\", ".").replaceAll(
					"/", ".");
			if (!classFilename.endsWith(".class")) {
				continue;
			}
			String inputClassname = classFilename.substring(0, classFilename
					.lastIndexOf("."));

			if (!StringUtils.hasText(this.includesClass)
					|| inputClassname.matches(this.includesClass)) {
				try {
					Class<?> clz = Class.forName(inputClassname);
					// clz = this.createLoader().loadClass(this.className);
					File outputFile = this.convertEntityToXml(clz);
					if (outputFile != null) {
						CallTarget ct = createCallTarget();
						Property dp = ct.createParam();
						dp.setName(this.outputDirId);
						dp.setValue(outputFile.getParent());
						this.log("Add param to antcall: " + this.outputDirId
								+ "[" + outputFile.getParent() + "]",
								Project.MSG_DEBUG);
						Property fp = ct.createParam();
						fp.setName(this.outputFileId);
						fp.setValue(outputFile.getName());
						this.log("Add param to antcall: " + this.outputFileId
								+ "[" + outputFile.getName() + "]",
								Project.MSG_DEBUG);
						ct.execute();
					} else {
						continue;
					}
				} catch (ClassNotFoundException e) {
					this.log(e.getMessage(), Project.MSG_ERR);
					throw new BuildException(e);
				} catch (LinkageError e) {
					this.log(e.getMessage(), Project.MSG_ERR);
					throw new BuildException(e);
				}
			}
		}

		this
				.log("End Run " + this.getClass().getSimpleName(),
						Project.MSG_INFO);
	}

	private File convertEntityToXml(Class<?> clz) {
		if (outputBasedir == null) {
			outputBasedir = this.getProject().getBaseDir().getAbsolutePath();
		}
		File file = new File(this.outputBasedir, clz.getName()
				.replace(".", "/")
				+ this.extension);
		if (!this.overwrite && file.exists()) {
			this.log("File[" + file.getPath() + "] is exist and skip it.",
					Project.MSG_INFO);
			return file;
		}
		try {
			// 不是实体映射则直接返回
			if (clz.isAnnotationPresent(Entity.class)) {
				this.writeXmlToFile(this.buildDoc(clz), file);
				// if (this.outputId != null) {
				// this.getProject().setNewProperty(this.outputId,
				// file.getAbsolutePath());
				// }
				return file;
			} else {
				if (file.exists()) {
					this.log("Delete exsit file: " + file.getAbsolutePath());
					file.delete();
				}
				// if (this.outputId != null) {
				// this.getProject().getProperties().remove(this.outputId);
				// }
				this.log("Ignore class [" + clz.getName() + "]");
				return null;
			}

		} catch (Exception e) {
			this.log(e.getMessage(), Project.MSG_ERR);
			throw new BuildException(e);
		}
	}

	private void writeXmlToFile(Document doc, File file)
			throws TransformerException, IOException {
		this.log("Begin Write to file: " + file.getAbsolutePath(),
				Project.MSG_VERBOSE);
		File dir = file.getParentFile();
		if (!dir.exists()) {
			dir.mkdirs();
			this.log("Create directory: " + dir.getAbsolutePath(),
					Project.MSG_VERBOSE);
		}
		Transformer transformer = this.createTransformer();
		Source source = new DOMSource(doc);
		Result result = new StreamResult(new BufferedWriter(
				new OutputStreamWriter(new FileOutputStream(file))));
		transformer.transform(source, result);
		this.log("Success output: " + file.getAbsolutePath(), Project.MSG_INFO);
		// Writer writer = new PrintWriter(System.out);
		// writer.close();
		this.log("End Write to file", Project.MSG_VERBOSE);
	}

	public Document buildDoc(Class<?> clz) throws ParserConfigurationException {
		this.log("Begin build document", Project.MSG_VERBOSE);
		// create root node
		DocumentBuilder builder = DocumentBuilderFactory.newInstance()
				.newDocumentBuilder();
		Document doc = builder.newDocument();
		this.buildRootNode(doc, clz);
		this.log("End build document", Project.MSG_VERBOSE);
		return doc;
	}

	private void buildRootNode(Document doc, Class<?> clz) {
		String logCnt = " build node: " + JpaEntityToXmlTask.NODE_ROOT;
		this.log("Begin" + logCnt, Project.MSG_VERBOSE);
		Element root = doc.createElementNS(
				"http://java.sun.com/xml/ns/persistence/orm",
				JpaEntityToXmlTask.NODE_ROOT);
		doc.appendChild(root);
		this.buildPackageNode(doc, root, clz);
		this.buildEntityNode(doc, root, clz);
		this.log("End" + logCnt, Project.MSG_VERBOSE);
	}

	private void buildPackageNode(Document doc, Element parent, Class<?> clz) {
		String logCnt = " build node: " + JpaEntityToXmlTask.NODE_PACKAGE + "["
				+ clz.getPackage().getName() + "]";
		this.log("Begin" + logCnt, Project.MSG_VERBOSE);
		// node: package
		Element pkg = doc.createElement(JpaEntityToXmlTask.NODE_PACKAGE);
		parent.appendChild(pkg);
		pkg.setTextContent(clz.getPackage().getName());
		this.log("End" + logCnt, Project.MSG_VERBOSE);
	}

	private void buildEntityNode(Document doc, Element parent, Class<?> clz) {
		String logCnt = " build node: " + JpaEntityToXmlTask.NODE_ENTITY + "["
				+ clz.getName() + "]";
		this.log("Begin" + logCnt, Project.MSG_VERBOSE);
		// node: entity
		Element entity = doc.createElement(JpaEntityToXmlTask.NODE_ENTITY);
		parent.appendChild(entity);
		// attribute: class
		entity.setAttribute(JpaEntityToXmlTask.ENTITY_ATTR_CLASS, clz
				.getSimpleName());

		// find id field and determine access type
		String accessType = null;
		Object idType = EntityIdUtil.locatingIdTypeByAnnotation(clz);
		if (idType == null) {
			accessType = this.defaultAccessType.toUpperCase();
		} else {
			if (idType instanceof Field) {
				accessType = JpaEntityToXmlTask.ENUM_ACCESS_FIELD;
			} else // if (idType instanceof PropertyDescriptor)
			{
				accessType = JpaEntityToXmlTask.ENUM_ACCESS_PROPERTY;
			}
		}
		entity.setAttribute(JpaEntityToXmlTask.ENTITY_ATTR_ACCESS, accessType);

		attrCache.clear();
		colCache.clear();
		Element table = this.buildTableNode(doc, entity, clz);
		Element attributes = this.buildAttributesNode(doc, entity, clz,
				accessType);

		Class<?> superClz = clz.getSuperclass();
		if (superClz.isAnnotationPresent(MappedSuperclass.class)) {
			table = this.buildEntityNodeRecusive(doc, entity, superClz, table,
					attributes, accessType);
		} else {
			this.log("Ignore Superclass: " + superClz.getName());
		}
		if (table == null) {// default table
			table = doc.createElement(NODE_TABLE);
			table.setAttribute(TABLE_NAME, clz.getSimpleName());
			this.log("Create default-node: " + NODE_TABLE + "["
					+ clz.getSimpleName() + "]", Project.MSG_VERBOSE);
		}
		for (String key : attrCache.keySet()) {// default column name
			if (!colCache.containsKey(key) && !Id.class.getName().equals(key)) {
				Element column = doc.createElement(NODE_COLUMN);
				column.setAttribute(COLUMN_NAME, key);
				attrCache.get(key).appendChild(column);
				this.log("Create default-node: " + NODE_COLUMN + "[" + key
						+ "]", Project.MSG_VERBOSE);
			}
		}
		entity.appendChild(table);
		entity.appendChild(attributes);
		this.log("End" + logCnt, Project.MSG_VERBOSE);
	}

	private Element buildEntityNodeRecusive(Document doc, Element parent,
			Class<?> clz, Element table, Element attributes, String accessType) {
		String logCnt = " build node: " + JpaEntityToXmlTask.NODE_ENTITY + "["
				+ clz.getName() + "]";
		this.log("Begin recusive" + logCnt, Project.MSG_VERBOSE);
		this.buildAttributesNodeRecusive(doc, attributes, clz, accessType);
		if (table == null) {
			table = this.buildTableNode(doc, parent, clz);
		}
		this.log("End recusive" + logCnt, Project.MSG_VERBOSE);
		return table;
	}

	private Element buildTableNode(Document doc, Element parent, Class<?> clz) {
		if (clz.isAnnotationPresent(Table.class)) {
			Element table = doc.createElement(NODE_TABLE);
			parent.appendChild(table);
			Table tableAnnot = clz.getAnnotation(Table.class);
			table
					.setAttribute(JpaEntityToXmlTask.TABLE_NAME, tableAnnot
							.name());
			this.log("Create build node: " + JpaEntityToXmlTask.NODE_TABLE
					+ "[" + tableAnnot.name() + "]", Project.MSG_VERBOSE);
			return table;
		}
		return null;
	}

	private Element buildAttributesNode(Document doc, Element parent,
			Class<?> clz, String accessType) {
		String logCnt = " build node: " + JpaEntityToXmlTask.NODE_ATTRIBUTES;
		this.log("Begin" + logCnt, Project.MSG_VERBOSE);
		Element attributes = doc
				.createElement(JpaEntityToXmlTask.NODE_ATTRIBUTES);
		parent.appendChild(attributes);
		this.buildAttributesNodeRecusive(doc, attributes, clz, accessType);
		this.log("End" + logCnt, Project.MSG_VERBOSE);
		return attributes;
	}

	private void buildAttributesNodeRecusive(Document doc, Element parent,
			Class<?> clz, String accessType) {
		String logCnt = " recursive build node: "
				+ JpaEntityToXmlTask.NODE_ATTRIBUTES;
		this.log("Begin" + logCnt, Project.MSG_VERBOSE);
		if (JpaEntityToXmlTask.ENUM_ACCESS_FIELD.equals(accessType)) {
			Field[] field = clz.getDeclaredFields();
			for (Field f : field) {
				this.buildAttributeNode(doc, parent, f, f.getName());
			}

		} else {
			Method[] ms = clz.getDeclaredMethods();
			for (Method m : ms) {
				if (Modifier.isPublic(m.getModifiers())) {
					String name = m.getName();
					if (name.startsWith("get")) {
						name = name.substring(3);
					} else if (name.startsWith("is")) {
						name = name.substring(2);
					} else {
						name = null;
					}
					if (name != null) {
						if (Character.isUpperCase(name.charAt(0))) {
							name = StringUtils.uncapitalize(name);
							this.buildAttributeNode(doc, parent, m, name);
						}
					}
				}
			}
		}
		this.log("End" + logCnt, Project.MSG_VERBOSE);
	}

	private void buildAttributeNode(Document doc, Element parent,
			AccessibleObject ao, String name) {
		if ("class".equals(name) || "serialVersionUID".equals(name)) {
			this.log("Not create node for accessibleObject[" + name + "] "
					+ " (The AccessibleObject[" + name + "])",
					Project.MSG_VERBOSE);
			return;
		}

		if (ao.isAnnotationPresent(Transient.class)) {
			this.log("Not create node for accessibleObject[" + name + "] "
					+ " (The AccessibleObject[" + name + "] is transient)",
					Project.MSG_VERBOSE);
			return;
		}

		if (ao.isAnnotationPresent(Id.class)) {
			this.buildIdNode(doc, parent, ao, name);
		} else {
			this.buildBasicNode(doc, parent, ao, name);
		}
	}

	private void buildIdNode(Document doc, Element parent, AccessibleObject ao,
			String name) {
		String logCnt = " build node: " + NODE_ID;
		this.log("Begin" + logCnt, Project.MSG_VERBOSE);
		Element id = attrCache.get(Id.class.getName());
		if (id != null) {
			this.log(
					"Not create node for accessibleObject[" + name + "] "
							+ " (The AccessibleObject[" + name
							+ "] is Id in subclass)", Project.MSG_VERBOSE);
			if (id.getAttribute(ID_NAME).equals(name)) {
				this.buildColumnNode(doc, id, ao, name);
			} else {
				this.buildBasicNode(doc, parent, ao, name);
			}
		} else {
			id = doc.createElement(JpaEntityToXmlTask.NODE_ID);
			this.log("Create new node: " + NODE_ID + "[" + name + "] ",
					Project.MSG_VERBOSE);
			parent.appendChild(id);
			id.setAttribute(JpaEntityToXmlTask.ID_NAME, name);
			this.buildColumnNode(doc, id, ao, name);
			if (attrCache.containsKey(name)) {
				Element oe = attrCache.get(name);
				Node p = oe.getParentNode();
				p.removeChild(oe);
				attrCache.remove(name);
			}
			attrCache.put(Id.class.getName(), id);
			attrCache.put(name, id);
		}
		this.log("End" + logCnt, Project.MSG_VERBOSE);
	}

	private void buildBasicNode(Document doc, Element parent,
			AccessibleObject ao, String name) {
		String logCnt = " build node: " + NODE_BASIC;
		this.log("Begin" + logCnt, Project.MSG_VERBOSE);
		Element basic = this.attrCache.get(name);
		if (basic != null) {
			this.log("Not create node for accessibleObject[" + name + "] "
					+ " (The AccessibleObject[" + name
					+ "] is exsit in subclass)", Project.MSG_VERBOSE);
			this.buildColumnNode(doc, basic, ao, name);
		} else {
			this.log("Create new node: " + NODE_BASIC + "[" + name + "]",
					Project.MSG_VERBOSE);
			basic = doc.createElement(JpaEntityToXmlTask.NODE_BASIC);
			basic.setAttribute(JpaEntityToXmlTask.BASIC_NAME, name);
			parent.appendChild(basic);
			this.buildColumnNode(doc, basic, ao, name);
			attrCache.put(name, basic);
		}
		this.log("End" + logCnt, Project.MSG_VERBOSE);
	}

	private void buildColumnNode(Document doc, Element parent,
			AccessibleObject ao, String name) {
		if (ao.isAnnotationPresent(Column.class)) {
			String logCnt = " build node: " + NODE_COLUMN;
			this.log("Begin" + logCnt, Project.MSG_VERBOSE);

			Element column = this.colCache.get(name);
			boolean isNew = false;
			if (column != null
					&& StringUtils.hasText(column
							.getAttribute(JpaEntityToXmlTask.COLUMN_NAME))) {
				this.log("Ignore node :" + JpaEntityToXmlTask.COLUMN_NAME
						+ "(Its Column name has been setted)",
						Project.MSG_VERBOSE);
			}

			if (column == null) {
				isNew = true;
				column = doc.createElement(JpaEntityToXmlTask.NODE_COLUMN);
				parent.appendChild(column);
			}
			Column colAnno = ao.getAnnotation(Column.class);
			column.setAttribute(JpaEntityToXmlTask.COLUMN_NAME, colAnno.name());
			colCache.put(name, column);
			this.log((isNew ? "Create" : "Update") + " node: "
					+ JpaEntityToXmlTask.NODE_COLUMN + "[" + colAnno.name()
					+ "]", Project.MSG_VERBOSE);

			this.log("End" + logCnt, Project.MSG_VERBOSE);
		}
	}

	private Transformer createTransformer() throws TransformerException {
		TransformerFactory tff = TransformerFactory.newInstance();
		tff.setAttribute("indent-number", 4);
		Transformer transformer = tff.newTransformer();

		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		if (this.encoding != null) {
			transformer.setOutputProperty(OutputKeys.ENCODING, this.encoding);
		}
		// if (this.publicId != null) {
		// transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC,
		// this.publicId);
		// }
		// if (this.systemId != null) {
		// transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,
		// this.systemId);
		// }
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		// transformer.setOutputProperty(OutputKeys.METHOD, "xml");
		return transformer;
	}

	private CallTarget createCallTarget() {
		CallTarget ct = (CallTarget) getProject().createTask("antcall");
		ct.init();
		ct.setTarget(this.target);
		ct.setOwningTarget(this.getOwningTarget());
		ct.setInheritAll(true);
		ct.setInheritRefs(true);
		return ct;

	}

	/**
	 * @param encoding
	 *            the encoding to set
	 */
	public void setEncoding(String encoding) {
		this.encoding = encoding;
	}

	/**
	 * @param outputPath
	 *            the outputPath to set
	 */
	public void setOutputBasedir(String outputPath) {
		Assert.hasText(outputPath);
		this.outputBasedir = outputPath;
	}

	/**
	 * @param className
	 *            the className to set
	 */
	public void setIncludesClass(String className) {
		this.includesClass = className;
	}

	/**
	 * @param outputId
	 *            the outputId to set
	 */
	public void setOutputDirId(String outputId) {
		this.outputDirId = outputId;
	}

	/**
	 * @param outputFileId
	 *            the outputFileId to set
	 */
	public void setOutputFileId(String outputFileId) {
		this.outputFileId = outputFileId;
	}

	/**
	 * @param extension
	 *            the extension to set
	 */
	public void setExtension(String extension) {
		this.extension = extension;
	}

	/**
	 * @param classBasedir
	 *            the classBasedir to set
	 */
	public void setClassBasedir(String classBasedir) {
		this.classBasedir = classBasedir;
	}

	/**
	 * @param target
	 *            the target to set
	 */
	public void setTarget(String target) {
		this.target = target;
	}

	/**
	 * @return the overwrite
	 */
	public void setOverwrite(boolean overwrite) {
		this.overwrite = overwrite;
	}

	/**
	 * @param defaultAccessType
	 *            the defaultAccessType to set
	 */
	public void setDefaultAccessType(String defaultAccessType) {
		if (defaultAccessType == null || "".equals(defaultAccessType)) {
			throw new IllegalArgumentException(
					"defaultAccessType can not be seted as empty");
		}
		this.defaultAccessType = defaultAccessType;
	}
}
